package com.mangix.files;

import com.mangix.conf.MangixConfig;
import com.mangix.utils.MangixUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.util.Iterator;
import java.util.UUID;

import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;

/**
 * Created by ZXL on 2014/5/15.
 */
public class MangixFiles {

    // Define the meta key of an object file.
    public static final String REPLICAS_NUM = "object.replicas_num";
//    public static final String REPLICAS_LOCATIONS = "object.replicas_locations";

    private Path[] dataDirs;
    private int M;

    public MangixFiles(int port) throws Exception {
        MangixConfig conf = MangixConfig.getInstance();
        M = conf.getM();
        String[] dirs = conf.getDataDirs();
        dataDirs = new Path[dirs.length];
        for (int i = 0; i < dirs.length; i++) {
            dataDirs[i] = Paths.get(dirs[i], port + "");
            if (!Files.exists(dataDirs[i]))
                Files.createDirectories(dataDirs[i]);
        }
    }

    public Path[] getDataDirs() {
        return this.dataDirs;
    }

    /**
     * Get the parent folder of a key.
     * @param key
     * @return
     */
    private String getFileFolderName(String key) {
        return Long.toString(MangixUtils.hash(key, M) & 0xff);
    }


    /**
     * Get available space of different data directories.
     * @return
     */
    public synchronized Long[] getAvailableSpace() {
        Long[] availableSpace = new Long[dataDirs.length];
        for (int i = 0; i < dataDirs.length; i++)
            try {
                availableSpace[i] = Files.getFileStore(dataDirs[i]).getUsableSpace();
            } catch (IOException e) {
                e.printStackTrace();
                availableSpace[i] = 0L;
            }
        return availableSpace;
    }

    /**
     * Get the total availableSpace of this node.
     * @return
     */
    public synchronized long getTotalAvailableSpace() {
        long totalSize = 0L;
        for (long size: getAvailableSpace()) {
            totalSize += size;
        }
        return totalSize;
    }

    /**
     * Count files in a directory (including files in all subdirectories)
     * @param directory the directory to start in
     * @return the total number of files
     */
    private int countFilesInDirectory(File directory) {
        int count = 0;
        for (File file : directory.listFiles()) {
            if (file.isFile()) {
                count++;
            }
            if (file.isDirectory()) {
                count += countFilesInDirectory(file);
            }
        }
        return count;
    }

    /**
     * Count how many objects in this node.
     * @return
     */
    public int countObjectsNumber() {
        int count = 0;
        for (Path dir : dataDirs) {
            count += countFilesInDirectory(dir.toFile());
        }
        return count;
    }

    /**
     * Try to get a file path that can be used to write an object to.
     * @param key
     * @param size
     * @return
     */
    public synchronized Path getWriteFilePath(String key, Long size) {
        Long[] availableSpace;
        availableSpace = getAvailableSpace();
        Long maxAvailableSpace = availableSpace[0];
        int pos = 0;
        for (int i = 1; i < availableSpace.length; i++)
            if (maxAvailableSpace < availableSpace[i]) {
                maxAvailableSpace = availableSpace[i];
                pos = i;
            }
        if (maxAvailableSpace < size)
            return null;
        return Paths.get(dataDirs[pos].toString(), getFileFolderName(key), key, Long.toString(System.currentTimeMillis()) + ".file");
    }

    public synchronized OutputStream getOutputStream(Path file) {
        try {
            Files.createDirectories(file.getParent());
            return new BufferedOutputStream(Files.newOutputStream(file, CREATE, APPEND));
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Get the latest object (including deleted file) of this key. If no file found, return null.
     * @param key
     * @return
     */
    public synchronized Path getLatestFile(String key) {
        Path latestFile = null;
        Long latest = 0L;
        for (Path dir : dataDirs) {
            Path folder;
            if (Files.exists(Paths.get(dir.toString(), getFileFolderName(key)))) {
                if (Files.isDirectory(Paths.get(dir.toString(), getFileFolderName(key), key))) {
                    folder =  Paths.get(dir.toString(), getFileFolderName(key), key);
                    DirectoryStream<Path> stream = null;
                    try {
                        stream = Files.newDirectoryStream(folder);
                        Iterator<Path> iterator = stream.iterator();
                        while (iterator.hasNext()) {
                            Path obj = iterator.next();
                            String[] temp = obj.getFileName().toString().split("\\.");
                            if (temp[1].equals("file") || temp[1].equals("delete")) {
                                Long version = Long.parseLong(obj.getFileName().toString().split("\\.")[0]);
                                if (latest < version) {
                                    latest = version;
                                    latestFile = obj;
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (stream != null) {
                            try {
                                stream.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        return latestFile;
    }

    /**
     *
     * @param key
     * @return
     */
    private synchronized Path getFileFolder(String key) {
        for (Path dir : dataDirs)
            if (Files.exists(Paths.get(dir.toString(), getFileFolderName(key))))
                if (Files.isDirectory(Paths.get(dir.toString(), getFileFolderName(key), key)))
                    return Paths.get(dir.toString(), getFileFolderName(key), key);
                else
                    return null;
        return null;
    }

    private synchronized Long getFolderSize(Path directory) throws IOException {
        Long length = (long) 0;
        DirectoryStream<Path> stream = null;
        try {
            stream = Files.newDirectoryStream(directory);
            Iterator<Path> iter = stream.iterator();
            while (iter.hasNext()) {
                Path obj = iter.next();
                if (Files.isDirectory(obj))
                    length += getFolderSize(obj);
                else
                    length += Files.size(obj);
            }
        } finally {
            stream.close();
        }
//        DirectoryStream<Path> stream = Files.newDirectoryStream(directory);
//        Iterator<Path> iter = stream.iterator();
//        while (iter.hasNext()) {
//            Path obj = iter.next();
//            if (Files.isDirectory(obj))
//                length += getFolderSize(obj);
//            else
//                length += Files.size(obj);
//        }
//        stream.close();
        return length;
    }

    public synchronized Long getUsedSpace() throws IOException {
        long totalUsedSpace = 0;
        for (int i = 0; i < dataDirs.length; i++) {
            totalUsedSpace += getFolderSize(dataDirs[i]);
        }
        return totalUsedSpace;
    }

    /**
     * Delete an object.
     * @param key
     * @return
     */
    public synchronized Boolean deleteObject(String key, int replicasNum) {
        Path folder = getFileFolder(key);
        if (folder == null)
            return false;
        Path tempFile = null;
        OutputStream out = null;
        try {
            tempFile = Paths.get(folder.toString(), UUID.randomUUID() + ".tmp");
            Path file = Paths.get(folder.toString(), Long.toString(System.currentTimeMillis()) + ".delete");
            Files.createFile(tempFile);
            out = Files.newOutputStream(tempFile, CREATE, APPEND);
            out.write(1);
            out.close();
            setReplicasNum(tempFile, replicasNum);
            Files.move(tempFile, file);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                Files.deleteIfExists(tempFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if(out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Remove all files that are older than deletedFileName.
     * @param key
     * @param deletedFileName
     * @return
     */
    public synchronized boolean removeDeletedObject(String key, String deletedFileName) {
        String[] temp = deletedFileName.split("\\.");
        if (temp.length != 2 || !temp[1].equals("delete")) {
            System.out.println("deletedFileName is not right: " + deletedFileName);
            return false;
        }
        long latestVersion = Long.parseLong(deletedFileName.split("\\.")[0]);
        for (Path dir : dataDirs) {
            Path keyFolder =  Paths.get(dir.toString(), getFileFolderName(key), key);
            if(Files.exists(keyFolder) && Files.isDirectory(keyFolder)) {
                DirectoryStream<Path> stream = null;
                try {
                    stream = Files.newDirectoryStream(keyFolder);
                    Iterator<Path> iterator = stream.iterator();
                    while (iterator.hasNext()) {
                        Path obj = iterator.next();
                        temp = obj.getFileName().toString().split("\\.");
                        if (temp[1].equals("file") || temp[1].equals("delete")) {
                            Long version = Long.parseLong(obj.getFileName().toString().split("\\.")[0]);
                            if (version <= latestVersion) {
                                Files.deleteIfExists(obj);
                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (stream != null) {
                        try {
                            stream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return true;
    }
    /**
     * Delete a folder if it is empty.
     * @param folder
     */
    public synchronized void deleteEmptyFolder(Path folder) {
        if(folder == null) return;
        if(Files.isDirectory(folder)) {
            try {
                Files.delete(folder);
                System.out.println("Delete empty folder: " + folder.toString());
            } catch (DirectoryNotEmptyException e) {
                return;
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
        }
    }

    /**
     * Write the replicas number of this object to the meta data of the file.
     * @param obj
     * @param replicasNum
     * @throws IOException
     */
     public synchronized void setReplicasNum(Path obj, int replicasNum) throws IOException {
        UserDefinedFileAttributeView view = Files.getFileAttributeView(obj, UserDefinedFileAttributeView.class);
        view.write(REPLICAS_NUM, Charset.defaultCharset().encode(String.valueOf(replicasNum)));
    }

    /**
     * Get the replicas number of an object.
     * @param obj
     * @return
     * @throws IOException
     */
     public synchronized int getReplicasNum(Path obj) throws IOException {
        UserDefinedFileAttributeView view = Files.getFileAttributeView(obj, UserDefinedFileAttributeView.class);
        ByteBuffer buf = ByteBuffer.allocate(view.size(REPLICAS_NUM));
        view.read(REPLICAS_NUM, buf);
        buf.flip();
        return Integer.parseInt(Charset.defaultCharset().decode(buf).toString());
    }

    /**
     * Delete all old versions in this file folder.
     */
    public synchronized void deleteOldFiles(Path dir) {
        Path latestFile = getLatestFile(dir.getFileName().toString());
        if (latestFile == null) {
            return;
        }
        Long latestVersion = Long.parseLong(latestFile.getFileName().toString().split("\\.")[0]);
        DirectoryStream<Path> stream = null;
        try {
            stream = Files.newDirectoryStream(dir);
            Iterator<Path> iterator = stream.iterator();
            while (iterator.hasNext()) {
                try {
                    Path file = iterator.next();
                    String[] fileName = file.getFileName().toString().split("\\.");
                    if (fileName.length == 2 && fileName[1].equals("tmp")) {
                        FileTime lastModifiedTime = Files.getLastModifiedTime(file);
                        // If the tmp file has existed for more than a day, delete it.
                        if (System.currentTimeMillis() - lastModifiedTime.toMillis() > 5184000000L) {
                            Files.delete(file);
                            System.out.println("Delete tmp file: " + file.toString());
                        }
                    } else if (fileName.length == 2 && (fileName[1].equals("file") || fileName[1].equals("delete"))) {
                        Long version = Long.parseLong(fileName[0]);
                        if (version < latestVersion) {
                            Files.delete(file);
                            System.out.println("Delete old file: " + file.toString());
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
